using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms;

namespace Shrapnel
{
    [Designer(typeof(ShrapnelDesigner))]
    [ToolboxItem(true)]
    [ToolboxBitmap("Shrapnel.bmp")]
	public partial class View : Control
    {
		public delegate Cable NewCableHandler(Connector from, Connector to);

		struct InitialViewState
		{
			public Point mousePosition;
			public ViewState viewState;
		}

        /// <summary>
        /// The view state of this workspace view.
        /// </summary>
        private ViewState viewState = new ViewState();

		/// <summary>
		/// The interaction manager to use for this workspace view.
		/// </summary>
		private Interaction.Manager interaction = new Interaction.Manager();

        /// <summary>
        /// The grid of this workspace view.
        /// </summary>
        private Grid grid = new Grid();

		/// <summary>
		/// The layers with all our elements on them.
		/// </summary>
		private List<Layer> layers = new List<Layer>();

		/// <summary>
		/// The minimum area that is used for selecting, this makes selecting objects with a 
		/// single click less "picky". It is specified in screen pixels.
		/// </summary>
		private float minimumSelectionSize = 8.0f;

		/// <summary>
		/// The automatic panner to use
		/// </summary>
		private AutomaticPanner automaticPanner;

		/// <summary>
		/// The amount of automatic panning
		/// </summary>
		private float automaticPanningSpeed = 5.0f;

		/// <summary>
		/// The interval (in milliseconds) between each automatic panning update. This more or less determines
		/// the smoothness of the automatic panning.
		/// </summary>
		private int automaticPanningInterval = 30;

		/// <summary>
		/// Event triggered upon creating a new cable, this event is responsible for returning a new cable instance.
		/// </summary>
		public event NewCableHandler NewCableRequired;

		/// <summary>
		/// Access to our layers with all elements on them, please note that the ordering
		/// of layers in this list determines the order in which they are drawn. Layer[0] is
		/// drawn first, Layer[N] is drawn on top of everything.
		/// </summary>
		internal List<Layer> Layers 
		{
			get { return layers; }
		}

		/// <summary>
		/// The minimum area that is used for selecting, this makes selecting objects with a 
		/// single click less "picky". It is specified in screen pixels.
		/// </summary>
		public float MinimumSelectionSize
		{
			get { return minimumSelectionSize; }
			set { minimumSelectionSize = value; }
		}

		/// <summary>
		/// Access to the interaction manager that is used
		/// </summary>
		public Interaction.Manager InteractionManager
		{
			get { return interaction; }
		}

		/// <summary>
		/// The amount of automatic panning
		/// </summary>
		public float AutomaticPanningSpeed
		{
			get { return automaticPanningSpeed; }
			set { automaticPanningSpeed = value; }
		}

		/// <summary>
		/// The interval (in milliseconds) between each automatic panning update. This more or less determines
		/// the smoothness of the automatic panning.
		/// </summary>
		public int AutomaticPanningInterval
		{
			get { return automaticPanningInterval; }
			set { automaticPanningInterval = value; }
		}

        /// <summary>
        /// Gets or sets the visibility of the grid.
        /// </summary>
        public bool GridVisible
        {
            get { return grid.Visible; }
            set { grid.Visible = value; Invalidate(); }
        }

        /// <summary>
        /// Gets or sets the size of the grid.
        /// </summary>
        public float GridSize
        {
            get { return grid.Size; }
            set { grid.Size = value; Invalidate(); }
        }



        /// <summary>
        /// Initializes a new instance of this class.
        /// </summary>
        public View()
        {
            SetStyle(ControlStyles.AllPaintingInWmPaint, true);
            SetStyle(ControlStyles.ContainerControl, true);
            SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
            SetStyle(ControlStyles.ResizeRedraw, true);
            SetStyle(ControlStyles.UserPaint, true);

            DoubleBuffered = true;

			// Hook up interaction handlers
			interaction.OnPanning.Start += OnPanningStart;
			interaction.OnPanning.Stop += OnPanningStop;
			interaction.OnPanning.Tick += OnPanningTick;

			interaction.OnZooming.Start += OnZoomingStart;
			interaction.OnZooming.Stop += OnZoomingStop;
			interaction.OnZooming.Tick += OnZoomingTick;

			interaction.OnSelecting.Start += OnSelectingStart;
			interaction.OnSelecting.Stop += OnSelectingStop;
			interaction.OnSelecting.Tick += OnSelectingTick;

			interaction.OnMoving.Start += OnMovingStart;
			interaction.OnMoving.Stop += OnMovingStop;
			interaction.OnMoving.Tick += OnMovingTick;

			interaction.OnWiring.Start += OnWiringStart;
			interaction.OnWiring.Stop += OnWiringStop;
			interaction.OnWiring.Tick += OnWiringTick;	

			interaction.OnFitAllInView += OnActionFitAllInView;
			interaction.OnFitSelectionInView += OnActionFitSelectionInView;
			interaction.OnDeleteSelection += OnActionDeleteSelection;
			interaction.OnSelectAll += SelectAll;
			interaction.OnDeselectAll += DeselectAll;
			interaction.OnCopySelection += CopySelection;
            interaction.OnCutSelection += CutSelection;
            interaction.OnPaste += PasteSelection;
            interaction.OnPrint += Print;

			automaticPanner = new AutomaticPanner(viewState, automaticPanningInterval);
			automaticPanner.OnUpdate += OnUpdateAutomaticPanner;

            OnInitializePrinting();
        }

        /// <summary>
        /// Adds a layer to the list of layers, but only if it's not added yet.
        /// </summary>
        /// <param name="layer">The layer that should be added to the list of layers.</param>
        public void AddLayer(Layer layer)
        {
            if (!layers.Contains(layer))
            {
                layers.Add(layer);
            }
        }

		/// <summary>
		/// Resets this workspace by removing all the elements.
		/// </summary>
		public void Reset()
		{
			layers.Clear();
			selection.Clear();
			viewState.ZoomFactor = 1.0f;
			viewState.Offset = new PointF(0, 0);
			interaction.Reset();
			ResetCopyBuffer();

			Invalidate();
		}


		/// <summary>
		/// Assure that everything fits in 95% of our view
		/// </summary>
		private void ZoomExtents(List<Element> elements)
		{
			bool noBboxFoundYet = true;
			BoundingBox bbox = new BoundingBox();

			// Create a spanning bounding box for all elements
			foreach (Element element in elements)
			{
				if (element is Node)
				{
					if (noBboxFoundYet)
					{
						bbox = element.GetBoundingBox();
						noBboxFoundYet = false;
					}
					else
					{
						bbox.Union(element.GetBoundingBox());
					}
				}
			}

			if (!noBboxFoundYet)
			{
				// Calculate a new zoomfactor and offset so our bbox will span 
				// exactly 95% of our view, centered at the view's center.

				SizeF viewSize = new SizeF(
					(float)Width * 0.95f,
					(float)Height * 0.95f);

				float zoomX = viewSize.Width / bbox.Size.Width;
				float zoomY = viewSize.Height / bbox.Size.Height;

				PointF worldCenter = new PointF(
					bbox.Location.X + bbox.Size.Width * 0.5f,
					bbox.Location.Y + bbox.Size.Height * 0.5f);
				PointF screenCenter = new PointF(
					(float)Width * 0.5f,
					(float)Height * 0.5f);

				viewState.ZoomFactor = Math.Min(zoomX, zoomY);
				viewState.Offset = new PointF(
					screenCenter.X - worldCenter.X * viewState.ZoomFactor,
					screenCenter.Y - worldCenter.Y * viewState.ZoomFactor);

				Invalidate();
			}
		}


		/// <summary>
		/// Update the automatic panning mechanism
		/// </summary>
		/// <param name="mousePosition"></param>
		private void UpdateAutomaticPanning(Point mousePosition)
		{
			bool shouldAutoPan = false;
			PointF direction = new PointF(0, 0);

			if (mousePosition.Y < 0)
			{
				shouldAutoPan = true;
				direction.Y = automaticPanningSpeed;
			}
			else if (mousePosition.Y >= Height)
			{
				shouldAutoPan = true;
				direction.Y = -automaticPanningSpeed;
			}

			if (mousePosition.X < 0)
			{
				shouldAutoPan = true;
				direction.X = automaticPanningSpeed;
			}
			else if (mousePosition.X >= Width)
			{
				shouldAutoPan = true;
				direction.X = -automaticPanningSpeed;
			}

			if (shouldAutoPan)
			{
				automaticPanner.Start(direction);
			} else 
			{
				automaticPanner.Stop();
			}
		}


		#region Mouse and keyboard event processing

		/// <summary>
		/// Determine the prevailing element type of the provided set of elements
		/// </summary>
		/// <param name="elements"></param>
		/// <returns></returns>
		private Interaction.AboveElementType GetAboveElementType(List<Element> elements, BoundingBox selectionBox)
		{
			// Determine types of objects we clicked on (a count per type)
			int numCables = 0;
			int numNodes = 0;
			int numConnectors = 0;

			foreach (Element element in elements)
			{
				if (element is Cable)
				{
					numCables++;					
				}
				else if (element is Node)
				{
					// Determine if the provided area contains a connector or not...
					Connector connector = ((Node)element).FindFirstOverlappingConnector(selectionBox);

					if (connector == null)
					{
						numNodes++;
					} else 
					{
						numConnectors++;
					}
				}
			}

			if (numConnectors > 0)
			{
				return Interaction.AboveElementType.Connector;
			} 
			else if (numNodes > 0)
			{
				return Interaction.AboveElementType.Node;
			} 
			else if (numCables > 0)
			{
				return Interaction.AboveElementType.Cable;
			} else 
			{
				return Interaction.AboveElementType.None;
			}
		}

		protected override void OnMouseDown(MouseEventArgs e)
		{
			base.OnMouseDown(e);

			Focus();

			// Determine if we clicked on any node, and which type prevails...
			PointF positionWorld = viewState.ScreenToWorld(new PointF(e.Location.X, e.Location.Y));
			BoundingBox selectionBox = new BoundingBox(positionWorld, new SizeF(1, 1));
			EnforceMinimumBoxSize(ref selectionBox);
			List<Element> selectedItems = GetOverlappingElements(selectionBox, false);
			Interaction.AboveElementType aboveType = GetAboveElementType(selectedItems, selectionBox);

			interaction.ProcessMouseDown(e, MouseButtons, ModifierKeys, aboveType);
		}

		protected override void OnMouseUp(MouseEventArgs e)
		{
			base.OnMouseUp(e);
			interaction.ProcessMouseUp(e, MouseButtons, ModifierKeys);
		}

		protected override void OnMouseMove(MouseEventArgs e)
		{
			base.OnMouseMove(e);
			interaction.ProcessMouseMove(e, MouseButtons, ModifierKeys);
		}

		protected override void OnKeyDown(KeyEventArgs e)
		{
			base.OnKeyDown(e);
			interaction.ProcessKeyDown(e, ModifierKeys);
		}

		#endregion


		private void OnActionFitAllInView()
		{
			ZoomAllInView();
		}

		private void OnActionFitSelectionInView()
		{
			ZoomSelectionInView();
		}

		private void OnActionDeleteSelection()
		{
			DeleteSelection();
		}


		#region Drawing

		protected override void OnPaint(PaintEventArgs e)
		{
			base.OnPaint(e);

			// Enforce anti-aliassing
			e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;


			// Draw white background
			e.Graphics.FillRectangle(Brushes.White, ClientRectangle);

			// Draw our grid
            if (grid.Visible)
            {
                grid.Draw(e, viewState);
            }

			// Draw our layers (in correct order!)
			for (int i = 0; i < layers.Count; ++i)
			{
				layers[i].Draw(e.Graphics, viewState);
			}

			switch (interaction.InteractionMode)
			{
				case Interaction.Mode.Normal:
					OnDrawNormalMode(ref e);
					break;

				case Interaction.Mode.Panning:
					OnDrawPanningMode(ref e);
					break;

				case Interaction.Mode.Selecting:
					OnDrawSelectingMode(ref e);
					break;

				case Interaction.Mode.Zooming:
					OnDrawZoomingMode(ref e);
					break;
			}
		}

		private void OnDrawNormalMode(ref PaintEventArgs e)
		{
			// TODO: Anything special should be drawn?
		}

		private void OnDrawPanningMode(ref PaintEventArgs e)
		{
			// TODO: We could draw an icon somewhere to indicate that we're panning.
		}

		private void OnDrawSelectingMode(ref PaintEventArgs e)
		{
			// TODO: We could draw an icon somewhere to indicate that we're selecting.

			// Let's draw the selection rectangle.
			selectionRectangle.Draw(e, viewState);
		}

		private void OnDrawZoomingMode(ref PaintEventArgs e)
		{
			// TODO: We could draw an icon somewhere to indicate that we're zooming.
		}

		#endregion


		private void InitializeComponent()
		{
			this.SuspendLayout();
			// 
			// View
			// 
			this.ResumeLayout(false);
		}


		/// <summary>
		/// Retrieves all elements that overlap the given rectangle
		/// </summary>
		/// <param name="rectangle">A rectangle in worldspace</param>
		/// <returns></returns>
		private List<Element> GetOverlappingElements(BoundingBox box, bool onlyNodes)
		{
			List<Element> overlappingElements = new List<Element>();

			foreach (Layer layer in layers)
			{
				foreach (Element element in layer)
				{
					if ((!onlyNodes || (element is Node)) &&
						element.OverlapsWith(box))
					{
						overlappingElements.Add(element);
					}
				}
			}

			return overlappingElements;
		}


		/// <summary>
		/// Enforce a minimum size to a bounding box, by growing it from the center.
		/// </summary>
		/// <param name="minimumSize"></param>
		/// <param name="box"></param>
		private void EnforceMinimumBoxSize(ref BoundingBox box)
		{
			float minimumSize = minimumSelectionSize / viewState.ZoomFactor;


			if (box.Size.Width < minimumSize)
			{
				box.Location = new PointF(
					box.Location.X - (minimumSize - box.Size.Width) * 0.5f,
					box.Location.Y);
				box.Size = new SizeF(minimumSize, box.Size.Height);
			}

			if (box.Size.Height < minimumSize)
			{
				box.Location = new PointF(
					box.Location.X,
					box.Location.Y - (minimumSize - box.Size.Height) * 0.5f);
				box.Size = new SizeF(box.Size.Width, minimumSize);
			}
		}


		/// <summary>
		/// Update handler for the automatic panner, called at a fixed frequence during automatic
		/// panning.
		/// </summary>
		private void OnUpdateAutomaticPanner()
		{
			// Simply force a repaint
			Invalidate();
		}

		/// <summary>
		/// Get all the cables that are attached to our selection, but are not actually
		/// selected.
		/// </summary>
		/// <returns></returns>
		private List<Cable> GetCablesAttachedToSelection()
		{
			List<Connector> selectedConnectors = new List<Connector>();

			// First find all connectors that belong to the selected nodes
			foreach (Element element in selection)
			{
				Node node = element as Node;
				if (node != null)
				{
					selectedConnectors.AddRange(node.Connectors);
				}
			}

			// Find all cables that are attached to any of the currently selected nodes,
			// but are not selected theirself.
			List<Cable> result = new List<Cable>();

			foreach (Layer layer in layers)
			{
				foreach (Element element in layer)
				{
					Cable cable = element as Cable;

					if ((cable != null) && !cable.IsSelected)
					{
						if (selectedConnectors.Contains(cable.From) ||
							selectedConnectors.Contains(cable.To))
						{
							result.Add(cable);
						}
					}
				}
			}

			return result;
		}

		/// <summary>
		/// Handle resize events
		/// </summary>
		/// <param name="e"></param>
		protected override void OnResize(EventArgs e)
		{
			base.OnResize(e);

			viewState.ViewportSize = new SizeF(this.Size.Width, this.Size.Height);
		}

	}
}
